/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.solr.cloud;

import com.carrotsearch.randomizedtesting.annotations.ThreadLeakLingering;
import com.carrotsearch.randomizedtesting.rules.SystemPropertiesRestoreRule;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.core.SolrCore;
import org.apache.solr.embedded.JettyConfig;
import org.apache.solr.embedded.JettySolrRunner;
import org.apache.solr.util.RevertDefaultThreadHandlerRule;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;

@ThreadLeakLingering(linger = 10)
@LuceneTestCase.SuppressSysoutChecks(bugUrl = "Solr logs to JUL")
@SolrTestCaseJ4.SuppressSSL(bugUrl = "https://issues.apache.org/jira/browse/SOLR-15026")
public class MiniSolrCloudClusterTest extends SolrTestCaseJ4 {

  @ClassRule
  public static TestRule solrClassRules =
      RuleChain.outerRule(new SystemPropertiesRestoreRule())
          .around(new RevertDefaultThreadHandlerRule());

  @Test
  public void testErrorsInStartup() throws Exception {

    AtomicInteger jettyIndex = new AtomicInteger();

    MiniSolrCloudCluster cluster = null;
    try {
      cluster =
          new MiniSolrCloudCluster(3, createTempDir(), JettyConfig.builder().build()) {
            @Override
            public JettySolrRunner startJettySolrRunner(
                String name, JettyConfig config, String solrXml) throws Exception {
              if (jettyIndex.incrementAndGet() != 2)
                return super.startJettySolrRunner(name, config, solrXml);
              throw new IOException("Fake exception on startup!");
            }
          };
      fail("Expected an exception to be thrown from MiniSolrCloudCluster");
    } catch (Exception e) {
      assertEquals("Incorrect exception", "Error starting up MiniSolrCloudCluster", e.getMessage());
      assertEquals("Expected one suppressed exception", 1, e.getSuppressed().length);
      assertEquals(
          "Incorrect suppressed exception",
          "Fake exception on startup!",
          e.getSuppressed()[0].getMessage());
    } finally {
      if (cluster != null) cluster.shutdown();
    }
  }

  @Test
  public void testErrorsInShutdown() throws Exception {

    AtomicInteger jettyIndex = new AtomicInteger();

    MiniSolrCloudCluster cluster =
        new MiniSolrCloudCluster(3, createTempDir(), JettyConfig.builder().build()) {
          @Override
          public JettySolrRunner stopJettySolrRunner(JettySolrRunner jetty) throws Exception {
            JettySolrRunner j = super.stopJettySolrRunner(jetty);
            if (jettyIndex.incrementAndGet() == 2)
              throw new IOException("Fake IOException on shutdown!");
            return j;
          }
        };

    Exception ex = expectThrows(Exception.class, cluster::shutdown);
    assertEquals("Error shutting down MiniSolrCloudCluster", ex.getMessage());
    assertEquals("Expected one suppressed exception", 1, ex.getSuppressed().length);
    assertEquals("Fake IOException on shutdown!", ex.getSuppressed()[0].getMessage());
  }

  @Test
  public void testExtraFilters() throws Exception {
    JettyConfig.Builder jettyConfig = JettyConfig.builder();
    jettyConfig.waitForLoadingCoresToFinish(null);
    jettyConfig.withFilter(JettySolrRunner.DebugFilter.class, "*");
    MiniSolrCloudCluster cluster =
        new MiniSolrCloudCluster(random().nextInt(3) + 1, createTempDir(), jettyConfig.build());
    cluster.shutdown();
  }

  public void testSolrHomeAndResourceLoaders() throws Exception {
    final String SOLR_HOME_PROP = "solr.solr.home";
    // regardless of what sys prop may be set, everything in the cluster should use solr home dirs
    // under the configured base dir -- and nothing in the call stack should be "setting" the sys
    // prop to make that work...
    final String fakeSolrHome = createTempDir().toAbsolutePath().toString();
    System.setProperty(SOLR_HOME_PROP, fakeSolrHome);

    // mock FS from createTempDir don't play nice using 'startsWith' when the solr stack
    // reconstitutes the path from string so we have to go the string route here as well...
    final Path workDir = Path.of(createTempDir().toAbsolutePath().toString());

    final MiniSolrCloudCluster cluster =
        new MiniSolrCloudCluster(1, workDir, JettyConfig.builder().build());
    try {
      final JettySolrRunner jetty = cluster.getJettySolrRunners().get(0);
      assertTrue(
          jetty.getCoreContainer().getSolrHome() + " vs " + workDir,
          // mock dirs from createTempDir() don't play nice with startsWith, so we have to use the
          // string value
          jetty.getCoreContainer().getSolrHome().startsWith(workDir));
      assertEquals(
          jetty.getCoreContainer().getSolrHome(),
          jetty.getCoreContainer().getResourceLoader().getInstancePath().toAbsolutePath());

      assertTrue(
          CollectionAdminRequest.createCollection("test", 1, 1)
              .process(cluster.getSolrClient())
              .isSuccess());
      final SolrCore core = jetty.getCoreContainer().getCores().get(0);
      assertTrue(
          core.getInstancePath() + " vs " + workDir, core.getInstancePath().startsWith(workDir));
      assertEquals(core.getInstancePath(), core.getResourceLoader().getInstancePath());
    } finally {
      cluster.shutdown();
    }
    assertEquals(
        "There is no reason why anything should have set this sysprop",
        fakeSolrHome,
        System.getProperty(SOLR_HOME_PROP));
  }

  public void testMultipleClustersDiffZk() throws Exception {
    final MiniSolrCloudCluster x =
        new MiniSolrCloudCluster(1, createTempDir(), JettyConfig.builder().build());
    try {
      final MiniSolrCloudCluster y =
          new MiniSolrCloudCluster(1, createTempDir(), JettyConfig.builder().build());
      try {

        // sanity check we're not talking to ourselves
        assertNotSame(x.getZkServer(), y.getZkServer());
        assertNotEquals(x.getZkServer().getZkAddress(), y.getZkServer().getZkAddress());

        // baseline check
        assertEquals(1, x.getJettySolrRunners().size());
        assertZkHost("x", x.getZkServer().getZkAddress(), x.getJettySolrRunners().get(0));

        assertEquals(1, y.getJettySolrRunners().size());
        assertZkHost("y", y.getZkServer().getZkAddress(), y.getJettySolrRunners().get(0));

        // adding nodes should be isolated
        final JettySolrRunner j2x = x.startJettySolrRunner();
        assertZkHost("x2", x.getZkServer().getZkAddress(), j2x);
        assertEquals(2, x.getJettySolrRunners().size());
        assertEquals(1, y.getJettySolrRunners().size());

        final JettySolrRunner j2y = y.startJettySolrRunner();
        assertZkHost("y2", y.getZkServer().getZkAddress(), j2y);
        assertEquals(2, x.getJettySolrRunners().size());
        assertEquals(2, y.getJettySolrRunners().size());
      } finally {
        y.shutdown();
      }
    } finally {
      x.shutdown();
    }
  }

  public void testJettyUsingSysProp() throws Exception {
    try {
      // this cluster will use a sysprop to communicate zkHost to it's nodes -- not node props in
      // the servlet context
      final MiniSolrCloudCluster x =
          new MiniSolrCloudCluster(1, createTempDir(), JettyConfig.builder().build()) {
            @Override
            public JettySolrRunner startJettySolrRunner(
                String name, JettyConfig config, String solrXml) throws Exception {
              System.setProperty("zkHost", getZkServer().getZkAddress());

              final Properties nodeProps = new Properties();
              nodeProps.setProperty("test-from-sysprop", "yup");

              Path runnerPath = createTempDir(name);
              if (solrXml == null) {
                solrXml = DEFAULT_CLOUD_SOLR_XML;
              }
              Files.write(runnerPath.resolve("solr.xml"), solrXml.getBytes(StandardCharsets.UTF_8));
              JettyConfig newConfig = JettyConfig.builder(config).build();
              JettySolrRunner jetty =
                  new JettySolrRunner(runnerPath.toString(), nodeProps, newConfig);
              return super.startJettySolrRunner(jetty);
            }
          };
      try {
        // baseline check
        assertEquals(1, x.getJettySolrRunners().size());
        assertZkHost("x", x.getZkServer().getZkAddress(), x.getJettySolrRunners().get(0));

        // verify MiniSolrCloudCluster's impl didn't change out from under us making test useless
        assertEquals(
            "yup",
            x.getJettySolrRunners().get(0).getNodeProperties().getProperty("test-from-sysprop"));
        assertNull(x.getJettySolrRunners().get(0).getNodeProperties().getProperty("zkHost"));

      } finally {
        x.shutdown();
      }
    } finally {
      System.clearProperty("zkHost");
    }
  }

  private static void assertZkHost(
      final String msg, final String zkHost, final JettySolrRunner node) {
    assertEquals(zkHost, node.getCoreContainer().getNodeConfig().getCloudConfig().getZkHost());
  }
}
